{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 向量库检索 or web 检索 agen\n",
    "- [x] 定制 CustomRetriever 类，接收 vectorstore 和 k 参数，实现多查询重排功能，llm生成的结果异常捕获，使用一次llm。  - [ ] 用llm对问题进行抽象\n",
    "- [x] 文档预处理和向量化\n",
    "- [ ] 检索prompt研究\n",
    "- [x] 开发api, use fastapi\n",
    "- [ ] 返回生成器对象\n",
    "- [ ] 制作多个性化聊天智能体\n",
    "- [ ] 拥有代码执行能力的智能体/目前没用\n",
    "- [ ] 多情景多轮对话\n",
    "- [ ] 多情感\n",
    "- [ ] prompt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from pprint import pprint\n",
    "from langchain_core.messages import AIMessage, HumanMessage\n",
    "from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
    "from langchain_community.document_loaders import WebBaseLoader\n",
    "from langchain_community.vectorstores import Chroma\n",
    "from langchain_nomic.embeddings import NomicEmbeddings\n",
    "from langchain.prompts import PromptTemplate\n",
    "from langchain_community.chat_models import ChatOllama\n",
    "from langchain_core.output_parsers import JsonOutputParser\n",
    "from langchain_core.output_parsers import StrOutputParser, ListOutputParser\n",
    "from langchain_core.retrievers import BaseRetriever\n",
    "from langchain_core.documents import Document\n",
    "from langchain_core.callbacks import CallbackManagerForRetrieverRun\n",
    "from typing import List,Any, Dict\n",
    "\n",
    "from typing import Annotated\n",
    "from typing_extensions import TypedDict\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "from langchain_core.runnables import chain\n",
    "llm2json = ChatOllama(model=\"qwen:7b\", format=\"json\", temperature=0)\n",
    "llm2str = ChatOllama(model=\"qwen2:7b\", temperature=0)\n",
    "os.environ[\"LANGCHAIN_TRACING_V2\"]=\"true\"\n",
    "os.environ[\"LANGCHAIN_ENDPOINT\"]=\"https://api.smith.langchain.com\"\n",
    "os.environ[\"LANGCHAIN_API_KEY\"]=\"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\"\n",
    "os.environ[\"LANGCHAIN_PROJECT\"]=\"pr-sunny-icicle-8\"\n",
    "os.environ[\"TAVILY_API_KEY\"] = \"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\"\n",
    "local_llm = \"qwen2:7b\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'问题组': ['详细列出RAG技术的技术流程步骤',\n",
       "  'RAG技术的整体工艺过程具体包括哪些环节?',\n",
       "  '想知道RAG技术从设计到完成的所有技术操作步骤',\n",
       "  'RAG技术的完整技术流程是怎样的，能提供一份详细的清单吗?'],\n",
       " '主题': 'RAG技术技术流程',\n",
       " '焦点': '整个流程和步骤',\n",
       " '目的': '获取详细流程信息',\n",
       " '意图': '理解并记录技术流程',\n",
       " '事件': '学习和技术应用',\n",
       " '行动': '查找、阅读和总结流程'}"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "prompt_multi_query = PromptTemplate(\n",
    "    input_variables=[\"question\"],\n",
    "    template=\"\"\"\n",
    "    * 你是一个问题要素提取并重写问题的助手，主要将原问题重写成4个新问题并用于检索\\n\n",
    "    * 你的任务流程是：\\\n",
    "    1. 提取用户问题的主题和焦点、目的和意图、事件和行动。返回的key为\"主题\"、\"焦点\"、\"目的\"、\"意图\"、\"事件\"和\"行动\"。\\\n",
    "    2. 对问题进行4次重写，要求这4个问题与原问题的相似度尽量高。返回的Key为\"问题组\",value为list，lists包含4个重写后的新问题\\\n",
    "    3. 输出一个json格式的字符串，格式为\"问题组\":[\"新问题1\",\"新问题2\",\"新问题3\",\"新问题4\"],\"主题\":\"主题\",\"焦点\":\"焦点\",\"目的\":\"目的\",\"意图\":\"意图\",\"事件\":\"事件\",\"行动\":\"行动\"\\\n",
    "\n",
    "\n",
    "\n",
    "    * 以下是需要重写的原问题:\n",
    "\n",
    "    {question}\n",
    "    \"\"\",)\n",
    "\n",
    "question_rewriter = prompt_multi_query | llm2json | JsonOutputParser()\n",
    "\n",
    "question_rewriter.invoke(\"一项RAG技术的整个技术流程是什么？\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 重写查询检索器\n",
    "from langchain.retrievers.self_query.base import SelfQueryRetriever\n",
    "class CustomRetriever(BaseRetriever):\n",
    "    vectorstore: Any  # 假设 vectorstore 是任意类型，实际类型应根据你的需要来定义\n",
    "    k: int\n",
    "    question_rewriter: Any  # 同样，这里的 Any 应该替换为实际的类型\n",
    "\n",
    "    def _get_relevant_documents(self, query: str) -> List[Document]:\n",
    "        \"\"\"\n",
    "        Get documents relevant to the query and add score information.\n",
    "\n",
    "        Args:\n",
    "            query (str): The search query.\n",
    "\n",
    "        Returns:\n",
    "            List[Document]: List of documents with updated scores.\n",
    "        \"\"\"\n",
    "        fused_scores = {}  # Document scores will be accumulated here\n",
    "\n",
    "        # Generate multiple queries using question_rewriter\n",
    "        query_dict = self._get_query_dict(query)\n",
    "\n",
    "        query_list = [query] + query_dict.get(\"问题组\", [])\n",
    "\n",
    "        for q in query_list:\n",
    "            docs, scores = zip(*self.vectorstore.similarity_search_with_score(q, k=3))\n",
    "\n",
    "            for doc, score in zip(docs, scores):\n",
    "                doc.metadata[\"score\"] = score\n",
    "\n",
    "            self._update_fused_scores(docs, scores, fused_scores)\n",
    "\n",
    "        # Re-rank documents based on the new scores\n",
    "        reranked_results = self._get_reranked_results(fused_scores)\n",
    "        return reranked_results[:self.k*2]\n",
    "\n",
    "    def _get_query_dict(self, query: str) -> Dict[str, Any]:\n",
    "        \"\"\"Generate query dictionary and handle potential errors.\"\"\"\n",
    "        try:\n",
    "            query_dict = self.question_rewriter.invoke(query)\n",
    "            if not isinstance(query_dict, dict) or \"问题组\" not in query_dict or not isinstance(query_dict[\"问题组\"], list):\n",
    "                raise ValueError(\"Invalid query_dict format\")\n",
    "            return query_dict\n",
    "        except (KeyError, ValueError) as e:\n",
    "            print(f\"Error: {e}\")\n",
    "            return {\"问题组\": []}\n",
    "\n",
    "    def _update_fused_scores(self, docs: List[Document], scores: List[float], fused_scores: Dict[str, Dict[str, float]]):\n",
    "        \"\"\"Update fused scores based on document scores and ranking.\"\"\"\n",
    "        doc_scores = {doc.page_content: doc.metadata for doc in docs}\n",
    "        \n",
    "        for rank, (doc, score) in enumerate(sorted(doc_scores.items(), key=lambda x: x[1][\"score\"], reverse=True)):\n",
    "            if doc not in fused_scores:\n",
    "                fused_scores[doc] = score\n",
    "                fused_scores[doc][\"score\"] = 0\n",
    "            fused_scores[doc][\"score\"] += 1.0 / (rank + 60)\n",
    "            print(fused_scores[doc][\"score\"])\n",
    "\n",
    "    def _get_reranked_results(self, fused_scores: Dict[str, Dict[str, float]]) -> List[Document]:\n",
    "        \"\"\"Re-rank documents based on the new scores.\"\"\"\n",
    "        return [Document(metadata=score, page_content=doc) for doc, score in\n",
    "                sorted(fused_scores.items(), key=lambda x: x[1][\"score\"], reverse=True)]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "## 检索可替换\n",
    "urls = [\n",
    "    \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
    "]\n",
    "import ollama\n",
    "docs = [WebBaseLoader(url).load() for url in urls]\n",
    "docs_list = [item for sublist in docs for item in sublist]\n",
    "print(docs_list)\n",
    "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
    "    chunk_size=250, chunk_overlap=0\n",
    ")\n",
    "doc_splits = text_splitter.split_documents(docs_list)\n",
    "print(doc_splits)\n",
    "# Debug the embedding object\n",
    "from langchain_ollama import OllamaEmbeddings\n",
    "\n",
    "embedding = OllamaEmbeddings(model=\"nomic-embed-text:v1.5\")\n",
    "print(type(embedding))  # Check the type of the embedding object\n",
    "print(embedding)  # Print the embedding object to see its structure\n",
    "\n",
    "# Add to vectorDB\n",
    "vectorstore = Chroma.from_documents(\n",
    "    documents=doc_splits,\n",
    "    collection_name=\"rag-chroma\",\n",
    "    embedding=embedding,  # Ensure embedding is correct\n",
    ")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.016666666666666666\n",
      "0.016666666666666666\n",
      "0.016666666666666666\n",
      "0.016666666666666666\n",
      "0.016666666666666666\n",
      "[Document(metadata={'description': 'The use of large language models in the real world has strongly accelerated by the launch of ChatGPT. We (including my team at OpenAI, shoutout to them) have invested a lot of effort to build default safe behavior into the model during the alignment process (e.g. via RLHF). However, adversarial attacks or jailbreak prompts could potentially trigger the model to output something undesired.\\nA large body of ground work on adversarial attacks is on images, and differently it operates in the continuous, high-dimensional space.', 'language': 'en', 'source': 'https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/', 'title': \"Adversarial Attacks on LLMs | Lil'Log\", 'score': 0.016666666666666666}, page_content='Fig. 13. UI for humans to do tool-assisted adversarial attack on a classifier. Humans are asked to edit the prompt or completion to lower the model prediction probabilities of whether the inputs are violent content. (Image source: Ziegler et al. 2022)\\nBot-Adversarial Dialogue (BAD; Xu et al. 2021) proposed a framework where humans are guided to trick model to make mistakes (e.g. output unsafe content). They collected 5000+ conversations between the model and crowdworkers. Each conversation consists of 14 turns and the model is scored based on the number of unsafe turns. Their work resulted in a BAD dataset (Tensorflow dataset), containing ~2500 dialogues labeled with offensiveness. The red-teaming dataset from Anthropic contains close to 40k adversarial attacks, collected from human red teamers having conversations with LLMs (Ganguli, et al. 2022). They found RLHF models are harder to be attacked as they scale up. Human expert red-teaming is commonly used for all safety preparedness work for big model releases at OpenAI, such as GPT-4 and DALL-E 3.\\nModel Red-teaming#'), Document(metadata={'description': 'The use of large language models in the real world has strongly accelerated by the launch of ChatGPT. We (including my team at OpenAI, shoutout to them) have invested a lot of effort to build default safe behavior into the model during the alignment process (e.g. via RLHF). However, adversarial attacks or jailbreak prompts could potentially trigger the model to output something undesired.\\nA large body of ground work on adversarial attacks is on images, and differently it operates in the continuous, high-dimensional space.', 'language': 'en', 'source': 'https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/', 'title': \"Adversarial Attacks on LLMs | Lil'Log\", 'score': 0.016666666666666666}, page_content='[16] Ganguli et al. “Red Teaming Language Models to Reduce Harms: Methods, Scaling Behaviors, and Lessons Learned.” arXiv preprint arXiv:2209.07858 (2022)\\n[17] Mehrabi et al. “FLIRT: Feedback Loop In-context Red Teaming.” arXiv preprint arXiv:2308.04265 (2023)\\n[18] Casper et al. “Explore, Establish, Exploit: Red Teaming Language Models from Scratch.” arXiv preprint arXiv:2306.09442 (2023)\\n[19] Xie et al. “Defending ChatGPT against Jailbreak Attack via Self-Reminder.” Research Square (2023)\\n[20] Jones et al. “Automatically Auditing Large Language Models via Discrete Optimization.” arXiv preprint arXiv:2303.04381 (2023)'), Document(metadata={'description': 'The use of large language models in the real world has strongly accelerated by the launch of ChatGPT. We (including my team at OpenAI, shoutout to them) have invested a lot of effort to build default safe behavior into the model during the alignment process (e.g. via RLHF). However, adversarial attacks or jailbreak prompts could potentially trigger the model to output something undesired.\\nA large body of ground work on adversarial attacks is on images, and differently it operates in the continuous, high-dimensional space.', 'language': 'en', 'source': 'https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/', 'title': \"Adversarial Attacks on LLMs | Lil'Log\", 'score': 0.016666666666666666}, page_content='Weng, Lilian. (Oct 2023). “Adversarial Attacks on LLMs”. Lil’Log. https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/.')]\n"
     ]
    }
   ],
   "source": [
    "retriever = CustomRetriever(\n",
    "    vectorstore=vectorstore,\n",
    "    k=3,\n",
    "    question_rewriter = question_rewriter\n",
    ")\n",
    "\n",
    "result = retriever.invoke(\"dinosaur movie with rating less than 8\")\n",
    "print(result)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 判断问题是否需要进行问题检索\n",
    "# 目前prompt合理，\n",
    "'''\n",
    "# tool 1：determine whether the qwen：7b can answer the question.\n",
    "qwen: 7b is a small llm, you can use 72b to answer the question.\n",
    "'''\n",
    "\n",
    "prompt_judege_retrieval = PromptTemplate(\n",
    "    input_variables=[\"question\"],\n",
    "    template=\"\"\"\n",
    "    如果你不知道或者不确定如何回答，直接回答\"no\"。如果你知道如何回答，请回答yes。\n",
    "\n",
    "\n",
    "    问题：\n",
    "\n",
    "\n",
    "    {question}\n",
    "    \"\"\",)\n",
    "\n",
    "judge_retrieval_chain = prompt_judege_retrieval | llm2str | StrOutputParser() | (lambda s: 'no' if 'no' in s.lower() else 'yes')\n",
    "# print((judge_retrieval_chain.invoke(\"我叫什么名字\")))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 96,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "list indices must be integers or slices, not str",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[0;32mIn[96], line 51\u001b[0m\n\u001b[1;32m     45\u001b[0m power_bot_answer_question \u001b[38;5;241m=\u001b[39m format_dialog \u001b[38;5;241m|\u001b[39m prompt_answer_question \u001b[38;5;241m|\u001b[39m llm2str \u001b[38;5;241m|\u001b[39m StrOutputParser()\n\u001b[1;32m     47\u001b[0m messages\u001b[38;5;241m=\u001b[39m[\n\u001b[1;32m     48\u001b[0m     HumanMessage(content\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m你好\u001b[39m\u001b[38;5;124m'\u001b[39m,\u001b[38;5;28mid\u001b[39m\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m0\u001b[39m\u001b[38;5;124m'\u001b[39m),\n\u001b[1;32m     49\u001b[0m ]\n\u001b[0;32m---> 51\u001b[0m \u001b[43mpower_bot_answer_question\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/anaconda3/envs/ChatTTS/lib/python3.10/site-packages/langchain_core/runnables/base.py:2871\u001b[0m, in \u001b[0;36mRunnableSequence.invoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m   2867\u001b[0m config \u001b[38;5;241m=\u001b[39m patch_config(\n\u001b[1;32m   2868\u001b[0m     config, callbacks\u001b[38;5;241m=\u001b[39mrun_manager\u001b[38;5;241m.\u001b[39mget_child(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mseq:step:\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mi\u001b[38;5;241m+\u001b[39m\u001b[38;5;241m1\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m   2869\u001b[0m )\n\u001b[1;32m   2870\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m i \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m-> 2871\u001b[0m     \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[43mstep\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m   2872\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m   2873\u001b[0m     \u001b[38;5;28minput\u001b[39m \u001b[38;5;241m=\u001b[39m step\u001b[38;5;241m.\u001b[39minvoke(\u001b[38;5;28minput\u001b[39m, config)\n",
      "File \u001b[0;32m/usr/local/anaconda3/envs/ChatTTS/lib/python3.10/site-packages/langchain_core/runnables/base.py:4436\u001b[0m, in \u001b[0;36mRunnableLambda.invoke\u001b[0;34m(self, input, config, **kwargs)\u001b[0m\n\u001b[1;32m   4422\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Invoke this Runnable synchronously.\u001b[39;00m\n\u001b[1;32m   4423\u001b[0m \n\u001b[1;32m   4424\u001b[0m \u001b[38;5;124;03mArgs:\u001b[39;00m\n\u001b[0;32m   (...)\u001b[0m\n\u001b[1;32m   4433\u001b[0m \u001b[38;5;124;03m    TypeError: If the Runnable is a coroutine function.\u001b[39;00m\n\u001b[1;32m   4434\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m   4435\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mhasattr\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfunc\u001b[39m\u001b[38;5;124m\"\u001b[39m):\n\u001b[0;32m-> 4436\u001b[0m     \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_call_with_config\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m   4437\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_invoke\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m   4438\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m   4439\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_config\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m   4440\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m   4441\u001b[0m \u001b[43m    \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m   4442\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m   4443\u001b[0m     \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\n\u001b[1;32m   4444\u001b[0m         \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot invoke a coroutine function synchronously.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m   4445\u001b[0m         \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mUse `ainvoke` instead.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m   4446\u001b[0m     )\n",
      "File \u001b[0;32m/usr/local/anaconda3/envs/ChatTTS/lib/python3.10/site-packages/langchain_core/runnables/base.py:1783\u001b[0m, in \u001b[0;36mRunnable._call_with_config\u001b[0;34m(self, func, input, config, run_type, **kwargs)\u001b[0m\n\u001b[1;32m   1779\u001b[0m     context \u001b[38;5;241m=\u001b[39m copy_context()\n\u001b[1;32m   1780\u001b[0m     context\u001b[38;5;241m.\u001b[39mrun(_set_config_context, child_config)\n\u001b[1;32m   1781\u001b[0m     output \u001b[38;5;241m=\u001b[39m cast(\n\u001b[1;32m   1782\u001b[0m         Output,\n\u001b[0;32m-> 1783\u001b[0m         \u001b[43mcontext\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m   1784\u001b[0m \u001b[43m            \u001b[49m\u001b[43mcall_func_with_variable_args\u001b[49m\u001b[43m,\u001b[49m\u001b[43m  \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m   1785\u001b[0m \u001b[43m            \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m  \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m   1786\u001b[0m \u001b[43m            \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m  \u001b[49m\u001b[38;5;66;43;03m# type: ignore[arg-type]\u001b[39;49;00m\n\u001b[1;32m   1787\u001b[0m \u001b[43m            \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m   1788\u001b[0m \u001b[43m            \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m   1789\u001b[0m \u001b[43m            \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m   1790\u001b[0m \u001b[43m        \u001b[49m\u001b[43m)\u001b[49m,\n\u001b[1;32m   1791\u001b[0m     )\n\u001b[1;32m   1792\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[1;32m   1793\u001b[0m     run_manager\u001b[38;5;241m.\u001b[39mon_chain_error(e)\n",
      "File \u001b[0;32m/usr/local/anaconda3/envs/ChatTTS/lib/python3.10/site-packages/langchain_core/runnables/config.py:383\u001b[0m, in \u001b[0;36mcall_func_with_variable_args\u001b[0;34m(func, input, config, run_manager, **kwargs)\u001b[0m\n\u001b[1;32m    381\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m run_manager \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m accepts_run_manager(func):\n\u001b[1;32m    382\u001b[0m     kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_manager\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m run_manager\n\u001b[0;32m--> 383\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
      "File \u001b[0;32m/usr/local/anaconda3/envs/ChatTTS/lib/python3.10/site-packages/langchain_core/runnables/base.py:4292\u001b[0m, in \u001b[0;36mRunnableLambda._invoke\u001b[0;34m(self, input, run_manager, config, **kwargs)\u001b[0m\n\u001b[1;32m   4290\u001b[0m                 output \u001b[38;5;241m=\u001b[39m chunk\n\u001b[1;32m   4291\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 4292\u001b[0m     output \u001b[38;5;241m=\u001b[39m \u001b[43mcall_func_with_variable_args\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m   4293\u001b[0m \u001b[43m        \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfunc\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\n\u001b[1;32m   4294\u001b[0m \u001b[43m    \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m   4295\u001b[0m \u001b[38;5;66;03m# If the output is a Runnable, invoke it\u001b[39;00m\n\u001b[1;32m   4296\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(output, Runnable):\n",
      "File \u001b[0;32m/usr/local/anaconda3/envs/ChatTTS/lib/python3.10/site-packages/langchain_core/runnables/config.py:383\u001b[0m, in \u001b[0;36mcall_func_with_variable_args\u001b[0;34m(func, input, config, run_manager, **kwargs)\u001b[0m\n\u001b[1;32m    381\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m run_manager \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mand\u001b[39;00m accepts_run_manager(func):\n\u001b[1;32m    382\u001b[0m     kwargs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrun_manager\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m run_manager\n\u001b[0;32m--> 383\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
      "Cell \u001b[0;32mIn[96], line 20\u001b[0m, in \u001b[0;36mformat_dialog\u001b[0;34m(conversation)\u001b[0m\n\u001b[1;32m     19\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mformat_dialog\u001b[39m(conversation:\u001b[38;5;28mlist\u001b[39m) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m \u001b[38;5;28mstr\u001b[39m:\n\u001b[0;32m---> 20\u001b[0m     conversation \u001b[38;5;241m=\u001b[39m \u001b[43mconversation\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mquestion\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[1;32m     21\u001b[0m     formatted_dialog \u001b[38;5;241m=\u001b[39m [\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m你是一个AI助手，\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m以下是用户和助手的对话：\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n\u001b[1;32m     22\u001b[0m     \u001b[38;5;28mprint\u001b[39m(conversation)\n",
      "\u001b[0;31mTypeError\u001b[0m: list indices must be integers or slices, not str"
     ]
    }
   ],
   "source": [
    "### 强大的llm用于回答问题\n",
    "\n",
    "# 构建检索用的prompt\n",
    "prompt_retrieve_augmented = PromptTemplate(\n",
    "    template=\"\"\"\n",
    "    你是一个检索增加机器人，你要基于检索的知识库来回答用户问题。\\n\\n\n",
    "\n",
    "    以下是检索到的知识：\n",
    "\n",
    "    {retrieved_knowledge}\\n\\n\n",
    "\n",
    "    用户问题：\n",
    "\n",
    "    {question}\n",
    "    \"\"\",\n",
    "    input_variables=[\"retrieved_knowledge\",\"question\"],\n",
    ")\n",
    "# 构建对话用的prompt\n",
    "def format_dialog(conversation:list) -> str:\n",
    "    '''\n",
    "    将langchain的message转化成我想要的message\n",
    "\n",
    "    args:\n",
    "        conversation: list[Message] example: {\"question\":[HumanMessage,AIMessage]}\n",
    "    retrun : str\n",
    "        exapmle:\n",
    "            \"你是一个AI助手，\\n\\n\\n以下是用户和助手的对话：\\n\\n\n",
    "            用户：你好\\n\\n\n",
    "            助手：你好，有什么可以帮你的吗？\\n\\n\n",
    "            以下是用户问题。\\n\\n\n",
    "            用户：我想知道如何使用langchain\"\n",
    "    raises:\n",
    "        TypeError: 如果 `conversation` 不是字典类型或缺少 'question' 键。\n",
    "        ValueError: 如果 'question' 键的值不是列表类型。\n",
    "        TypeError: 如果 'question' 列表中包含非 HumanMessage 或 AIMessage 实例。\n",
    "    '''\n",
    "\n",
    "        # 输入检查\n",
    "    if not isinstance(conversation, dict):\n",
    "        raise TypeError(\"输入参数 `conversation` 必须是一个字典。\")\n",
    "    \n",
    "    if 'question' not in conversation:\n",
    "        raise KeyError(\"字典中缺少 'question' 键。\")\n",
    "    \n",
    "    if not isinstance(conversation['question'], list):\n",
    "        raise ValueError(\"'question' 键的值必须是一个列表。\")\n",
    "    \n",
    "    # 处理对话列表\n",
    "    dialog_list = conversation['question']\n",
    "    if not all(isinstance(message, (HumanMessage, AIMessage)) for message in dialog_list):\n",
    "        raise TypeError(\"'question' 列表中的每个元素必须是 HumanMessage 或 AIMessage 实例。\")\n",
    "    \n",
    "    conversation = conversation['question']\n",
    "    formatted_dialog = [\"你是一个AI助手，\\n\\n\\n以下是用户和助手的对话：\"]\n",
    "\n",
    "    question = conversation.pop(-1)\n",
    "\n",
    "    for message in conversation:\n",
    "        if isinstance(message, HumanMessage):\n",
    "            formatted_dialog.append(f\"用户：{message.content}\")\n",
    "        elif isinstance(message, AIMessage):\n",
    "            formatted_dialog.append(f\"助手：{message.content}\\n\\n\")\n",
    "            \n",
    "    formatted_dialog.append(f\"以下是用户问题。\\n\\n用户：{question.content}\")\n",
    "    return \"\\n\".join(formatted_dialog)\n",
    "\n",
    "\n",
    "# 构建回答用的prompt\n",
    "prompt_answer_question = PromptTemplate(\n",
    "    template=\"\"\"\n",
    "    {question}。\n",
    "    \"\"\",\n",
    "    input_variables=[\"question\"],\n",
    ")\n",
    "\n",
    "power_bot_retrieved_knowledge = prompt_retrieve_augmented | llm2str | StrOutputParser()\n",
    "power_bot_answer_question = format_dialog | prompt_answer_question | llm2str | StrOutputParser()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/anaconda3/envs/ChatTTS/lib/python3.10/site-packages/langchain_core/_api/deprecation.py:139: LangChainDeprecationWarning: The method `BaseRetriever.get_relevant_documents` was deprecated in langchain-core 0.1.46 and will be removed in 0.3.0. Use invoke instead.\n",
      "  warn_deprecated(\n"
     ]
    }
   ],
   "source": [
    "### 向量库检索文档\n",
    "\n",
    "question = \"hello\"\n",
    "docs = retriever.get_relevant_documents(question)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "metadata": {},
   "outputs": [],
   "source": [
    "### web检索文档\n",
    "\n",
    "\n",
    "web_search_tool = TavilySearchResults(k=3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {},
   "outputs": [],
   "source": [
    "### 文档评分，后续可换计算公式，目前用llm判断\n",
    "prompt_doc_grade = PromptTemplate(\n",
    "    template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
    "    Here is the retrieved document: \\n\\n {document} \\n\\n\n",
    "    Here is the user question: {question} \\n\n",
    "    If the document contains keywords related to the user question, grade it as relevant. \\n\n",
    "    It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \\n\n",
    "    Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question. \\n\n",
    "    Provide the binary score as a JSON with a single key 'score' and no premable or explanation.\"\"\",\n",
    "    input_variables=[\"question\", \"document\"],\n",
    ")\n",
    "\n",
    "retrieval_grader = prompt_doc_grade | llm2json | JsonOutputParser()\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {},
   "outputs": [],
   "source": [
    "### 路由网页搜索 or rewrite question。目前用llm来判断上下文与问题的相关度，有低中高。\n",
    "prompt_router = PromptTemplate(\n",
    "    template=\"\"\"你是一个专家，需要你来判断用户给出的问题是否与提供的文档相关。\\n\n",
    "    这里是文档。\\n\n",
    "    {context} \\n\n",
    "    需要你返回相关度标准，如果相关度高则返回high，如果相关度中等则返回medium，如果相关度低则返回low。 \\n\n",
    "    Return the a JSON with a single key 'relevance' and no premable or explanation. value only one of the high,medium,low\\n\n",
    "    这是用户给出的问题:\\n \n",
    "    {question}\"\"\",\n",
    "    input_variables=[\"context\",\"question\"],\n",
    ")\n",
    "\n",
    "question_router = prompt_router | llm2json | JsonOutputParser()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Question Re-writer\n",
    "\n",
    "# LLM\n",
    "\n",
    "# Prompt\n",
    "re_write_prompt = PromptTemplate(\n",
    "    template=\"\"\"You a question re-writer that converts an input question to a better version that is optimized \\n \n",
    "     for vectorstore retrieval. Look at the initial and formulate an improved question. \\n\n",
    "     Here is the initial question: \\n\\n {question}. Improved question with no preamble: \\n \"\"\",\n",
    "    input_variables=[\"question\"],\n",
    ")\n",
    "\n",
    "question_rewriter = re_write_prompt | llm2str | StrOutputParser()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# graph state"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "\n",
    "class GraphState(TypedDict):\n",
    "    \"\"\"\n",
    "    Represents the state of our graph.\n",
    "\n",
    "    Attributes:\n",
    "        question: question\n",
    "        generation: LLM generation\n",
    "        documents: list of documents\n",
    "    \"\"\"\n",
    "\n",
    "    messages: Annotated[list, add_messages]\n",
    "    documents: List[str]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 105,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Nodes\n",
    "def power_bot(state):\n",
    "    \"\"\"\n",
    "    Power bot: Powerful llm to Answer question directly\n",
    "    \n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): New key added to state, generation, that contains LLM generation \n",
    "    \"\"\"\n",
    "    print(\"---POWER BOT ANSWERING---\")\n",
    "    question = state[\"messages\"]\n",
    "    documents = state[\"documents\"]\n",
    "    if documents:\n",
    "        print(\"---DOCUMENTS FOUND---\")\n",
    "        generation = power_bot_retrieved_knowledge.invoke({\"context\": documents,\"question\": question})\n",
    "    else: \n",
    "        print(\"---NO DOCUMENTS FOUND---\")\n",
    "        generation = power_bot_answer_question.invoke({\"question\": question})\n",
    "    return {\"documents\": documents, \"messages\": AIMessage(content = generation)}\n",
    "\n",
    "def retrieve(state):\n",
    "    \"\"\"\n",
    "    Retrieve documents\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): New key added to state, documents, that contains retrieved documents\n",
    "    \"\"\"\n",
    "    print(\"---RETRIEVE---\")\n",
    "    question = state[\"messages\"][-1]\n",
    "\n",
    "    # Retrieval\n",
    "    documents = retriever.get_relevant_documents(question)\n",
    "    return {\"documents\": documents}\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "def grade_documents(state):\n",
    "    \"\"\"\n",
    "    Determines whether the retrieved documents are relevant to the question.\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): Updates documents key with only filtered relevant documents\n",
    "    \"\"\"\n",
    "\n",
    "    print(\"---CHECK DOCUMENT RELEVANCE TO QUESTION---\")\n",
    "    question = state[\"messages\"][-1]\n",
    "    documents = state[\"documents\"]\n",
    "\n",
    "    # Score each doc\n",
    "    filtered_docs = []\n",
    "    for d in documents:\n",
    "        score = retrieval_grader.invoke(\n",
    "            {\"question\": question, \"document\": d.page_content}\n",
    "        )\n",
    "        grade = score[\"score\"]\n",
    "        if grade == \"yes\":\n",
    "            print(\"---GRADE: DOCUMENT RELEVANT---\")\n",
    "            filtered_docs.append(d)\n",
    "        else:\n",
    "            print(\"---GRADE: DOCUMENT NOT RELEVANT---\")\n",
    "            continue\n",
    "    return {\"documents\": filtered_docs}\n",
    "\n",
    "\n",
    "def transform_query(state):\n",
    "    \"\"\"\n",
    "    Transform the query to produce a better question.\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): Updates question key with a re-phrased question\n",
    "    \"\"\"\n",
    "\n",
    "    print(\"---TRANSFORM QUERY---\")\n",
    "    question = state[\"messages\"][-1]\n",
    "    documents = state[\"documents\"]\n",
    "\n",
    "    # Re-write question\n",
    "    better_question = question_rewriter.invoke({\"question\": question})\n",
    "    # state[\"message\"][-1] = better_question\n",
    "    return {\"documents\": documents, \"messages\": better_question}\n",
    "\n",
    "def web_search (state):\n",
    "    \"\"\"\n",
    "    Web search to find documents.\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        state (dict): New key added to state, documents, that contains retrieved documents\n",
    "    \"\"\"\n",
    "    print(\"---WEB SEARCH---\")\n",
    "    # question = state[\"question\"]\n",
    "    # print(question)\n",
    "    # documents = web_search_tool.invoke({\"query\": question})\n",
    "    # print(documents)\n",
    "    # web_results = '\\n'.join([d[\"content\"] for d in documents])\n",
    "    # web_results = Doucument(page_content=web_results)\n",
    "    from langchain.schema import Document\n",
    "    question = state[\"question\"]\n",
    "    print(question)\n",
    "    docs = web_search_tool.invoke({\"query\": question})\n",
    "    web_results = \"\\n\".join([d[\"content\"] for d in docs])\n",
    "    web_results = Document(page_content=web_results)\n",
    "    return {\"documents\": web_results}\n",
    "\n",
    "### Edges\n",
    "def judge_retrieval(state):\n",
    "    \"\"\"\n",
    "    Determines whether to route question to derectly answer or RAG.\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        str: Next node to call\n",
    "    \"\"\"\n",
    "    print(\"---JUDGE QUESTION---\")\n",
    "    question = state[\"messages\"]\n",
    "    if judge_retrieval_chain.invoke({\"question\": question})== \"yes\":\n",
    "        print(\"---JUDGE: QUESTION IS DIRECTLY ANSWERABLE---\")\n",
    "        return \"power_bot\"\n",
    "    else:\n",
    "        print(\"---JUDGE: QUESTION IS NOT DIRECTLY ANSWERABLE---\")\n",
    "        return \"power_bot\"\n",
    "\n",
    "def route_question(state):\n",
    "    \"\"\"\n",
    "    Route question to web search or RAG.\n",
    "\n",
    "    Args:\n",
    "        state (dict): The current graph state\n",
    "\n",
    "    Returns:\n",
    "        str: Next node to call\n",
    "    \"\"\"\n",
    "\n",
    "    print(\"---ROUTE QUESTION---\")\n",
    "    question = state[\"messages\"][-1]\n",
    "    documents = state[\"documents\"]\n",
    "    source = question_router.invoke({\"context\": documents, \"question\": question})\n",
    "    print(documents)\n",
    "    print(question)\n",
    "    print(source)\n",
    "    print(source[\"relevance\"])\n",
    "    if source[\"relevance\"] == \"low\":\n",
    "        print(\"---ROUTE QUESTION TO WEB SEARCH---\")\n",
    "        return \"web_search\"\n",
    "    elif source[\"relevance\"] == \"medium\":\n",
    "        print(\"---ROUTE QUESTION TO rewrite question---\")\n",
    "        return \"rewrite\"\n",
    "    elif source[\"relevance\"] == \"high\":\n",
    "        print(\"---ROUTE TO ANSWER THE QUESTION---\")\n",
    "        return \"power_bot\"\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 86,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.checkpoint.sqlite import SqliteSaver\n",
    "\n",
    "memory = SqliteSaver.from_conn_string(\":memory:\")\n",
    "config = {\"configurable\": {\"thread_id\": \"1\"}}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 106,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import END, StateGraph, START\n",
    "\n",
    "workflow = StateGraph(GraphState)\n",
    "\n",
    "# Define the nodes\n",
    "workflow.add_node(\"power_bot\", power_bot)\n",
    "workflow.add_node(\"retrieve\", retrieve)  # retrieve\n",
    "workflow.add_node(\"grade_documents\", grade_documents)  # grade documents\n",
    "workflow.add_node(\"web_search\", web_search)\n",
    "workflow.add_node(\"transform_query\", transform_query)  # transform_query\n",
    "\n",
    "# Build graph\n",
    "#workflow.add_edge(START, \"retrieve\")\n",
    "workflow.add_edge(\"power_bot\",END)\n",
    "workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
    "workflow.add_conditional_edges(\n",
    "    START,\n",
    "    judge_retrieval,\n",
    "    {\n",
    "        \"power_bot\": \"power_bot\",\n",
    "        \"vectorstore\": \"retrieve\",\n",
    "    },\n",
    ")\n",
    "workflow.add_conditional_edges(\n",
    "    \"grade_documents\",\n",
    "    route_question,\n",
    "    {\n",
    "        \"web_search\": \"web_search\",\n",
    "        \"rewrite\": \"transform_query\",\n",
    "        \"power_bot\": \"power_bot\",\n",
    "    },\n",
    ")\n",
    "workflow.add_edge(\"transform_query\", \"retrieve\")\n",
    "workflow.add_edge(\"web_search\", \"power_bot\")\n",
    "\n",
    "# Compile\n",
    "app = workflow.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "try:\n",
    "    display(Image(app.get_graph().draw_mermaid_png()))\n",
    "except Exception:\n",
    "    # This requires some extra dependencies and is optional\n",
    "    print(\"out\")\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 107,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "---JUDGE QUESTION---\n",
      "---JUDGE: QUESTION IS NOT DIRECTLY ANSWERABLE---\n",
      "content='你还记得我是谁吗' id='da068479-a560-4984-8adb-9ad86ee42a52'\n",
      "'\\n---\\n'\n",
      "---POWER BOT ANSWERING---\n",
      "---NO DOCUMENTS FOUND---\n",
      "[HumanMessage(content='你还记得我是谁吗', id='76a3c3c7-b27b-4530-a1a2-e85693bbd974'), HumanMessage(content='你还记得我是谁吗', id='794145a9-bad9-4b9f-b562-8a19cc0f24ab'), HumanMessage(content='你还记得我是谁吗', id='2225c896-bb15-4966-a072-a00f7701a349'), HumanMessage(content='你还记得我是谁吗', id='6578f5bb-d39d-42ff-a4af-1921b9f4a2bd'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答问题。您需要什么方面的帮助呢？', id='6c4d62e1-a346-4d56-8db4-8309d8d3e987'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以无法记住特定的人或对话。但我可以继续为您提供帮助和解答您的问题。请问您有什么需要了解或者想要解决的问题呢？', id='8a8fc39d-495f-4cf1-a5d6-0012b34786e5'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答问题。如果您有具体的问题需要解答，请告诉我，我会尽力为您提供帮助。您需要了解什么方面的信息呢？', id='aaeedbf0-1d5b-47ac-b933-34b7efa68e27'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答您的问题。如果您有具体的问题或者需要讨论某个话题，请告诉我，我会尽力为您提供所需的信息或解答。您现在想要了解什么内容呢？', id='630bc8ab-3172-4a0e-b188-52409aeb4b18'), HumanMessage(content='你还记得我是谁吗', id='da068479-a560-4984-8adb-9ad86ee42a52')]\n",
      "<class 'list'>\n",
      "content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答问题。如果您有具体的问题需要解答或者想要讨论某个话题，请告诉我，我会尽力为您提供所需的信息或解答。您现在想要了解什么内容呢？' id='f4c61394-9b5a-4809-beec-7ecf24aa62b0'\n",
      "'\\n---\\n'\n"
     ]
    }
   ],
   "source": [
    "\n",
    "# Run\n",
    "user_input = \"你还记得我是谁吗\"\n",
    "inputs = {\"messages\": [HumanMessage(content = user_input)]}\n",
    "for output in app.stream(inputs,{\"configurable\": {\"thread_id\": \"114\"}}, stream_mode=\"values\"):\n",
    "    print(output[\"messages\"][-1])\n",
    "        # Optional: print full state at each node\n",
    "        # pprint.pprint(value[\"keys\"], indent=2, width=80, depth=None)\n",
    "    pprint(\"\\n---\\n\")\n",
    "\n",
    "# Final generation\n",
    "# print((output))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from fastapi import FastAPI\n",
    "import uvicorn\n",
    "\n",
    "app = FastAPI()\n",
    "\n",
    "@app.get(\"/\")\n",
    "def llm(query: str = None,uuid: str = None):\n",
    "    inputs = {\"messages\": [HumanMessage(content = query)]}\n",
    "\n",
    "    for output in app.stream(inputs,{\"configurable\": {\"thread_id\": uuid}}, stream_mode=\"values\"):\n",
    "        print(output[\"messages\"][-1])\n",
    "\n",
    "    # return EventSourceResponse(generator, media_type=\"text/event-steam\") # 返回生成器，待实现\n",
    "    return output[\"messages\"][-1]\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    uvicorn.run(app, host=\"127.0.0.1\", port=8000, reload=True)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'messages': [HumanMessage(content='Hi there! My name is Will.', id='75b3257d-e09f-47f4-aeba-8898e69c3d8a'), HumanMessage(content='Hello, Will! Nice to meet you. How can I assist you today?', id='02f85ebc-6519-48c6-8372-713d06e21cc0'), HumanMessage(content='Hi there! My name is Will.', id='3ca51a38-00cc-4596-bb94-53e39d353122'), HumanMessage(content='你好！很高兴遇见你，Will。我是这里的专家，有什么我可以帮助你的吗？', id='c3efbfa5-0159-4d93-8dc9-9dc9bec39e63'), HumanMessage(content='Hi there! My name is Will.', id='be1f8bf1-d618-4076-84da-63daeee9a175'), HumanMessage(content='你好！Will，很高兴遇见你。我是这里的专家，有什么我可以帮助你的吗？请告诉我你的需求或者你想了解的问题，我会尽力提供最合适的解答和建议。无论是技术上的难题、学习资源的需求还是任何其他疑问，都欢迎提问哦！', id='e15f0e8e-dec0-4607-949b-946528f4c2ab'), HumanMessage(content='Hi there! My name is Will.', id='365d68be-7f55-41c2-9971-4e51542c5039'), HumanMessage(content='你好！Will，很高兴遇见你。我是这里的专家，有什么我可以帮助你的吗？请告诉我你的需求或者你想了解的问题，我会尽力提供最合适的解答和建议。无论是技术上的难题、学习资源的需求还是任何其他疑问，都欢迎提问哦！\\n\\n如果你有任何具体问题或需要指导的领域，请详细描述一下，这样我就能更准确地为你提供帮助了。无论是编程语言、算法、数据结构、软件开发流程、项目管理方法，还是其他相关主题，我都将竭尽全力为你解答和提供建议。\\n\\n请随时告诉我你的需求，让我们一起探索知识的海洋吧！', id='6135950e-c7a1-48e5-b797-c81de0a65e26')], 'documents': None}\n"
     ]
    }
   ],
   "source": [
    "print((output))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 108,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "StateSnapshot(values={'messages': [HumanMessage(content='你还记得我是谁吗', id='76a3c3c7-b27b-4530-a1a2-e85693bbd974'), HumanMessage(content='你还记得我是谁吗', id='794145a9-bad9-4b9f-b562-8a19cc0f24ab'), HumanMessage(content='你还记得我是谁吗', id='2225c896-bb15-4966-a072-a00f7701a349'), HumanMessage(content='你还记得我是谁吗', id='6578f5bb-d39d-42ff-a4af-1921b9f4a2bd'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答问题。您需要什么方面的帮助呢？', id='6c4d62e1-a346-4d56-8db4-8309d8d3e987'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以无法记住特定的人或对话。但我可以继续为您提供帮助和解答您的问题。请问您有什么需要了解或者想要解决的问题呢？', id='8a8fc39d-495f-4cf1-a5d6-0012b34786e5'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答问题。如果您有具体的问题需要解答，请告诉我，我会尽力为您提供帮助。您需要了解什么方面的信息呢？', id='aaeedbf0-1d5b-47ac-b933-34b7efa68e27'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答您的问题。如果您有具体的问题或者需要讨论某个话题，请告诉我，我会尽力为您提供所需的信息或解答。您现在想要了解什么内容呢？', id='630bc8ab-3172-4a0e-b188-52409aeb4b18'), AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答问题。如果您有具体的问题需要解答或者想要讨论某个话题，请告诉我，我会尽力为您提供所需的信息或解答。您现在想要了解什么内容呢？', id='f4c61394-9b5a-4809-beec-7ecf24aa62b0')]}, next=(), config={'configurable': {'thread_id': '114', 'thread_ts': '1ef49a25-499b-6204-8019-01718495059c'}}, metadata={'source': 'loop', 'step': 25, 'writes': {'power_bot': {'messages': AIMessage(content='我作为一个AI助手，并没有个人记忆，所以我无法记住特定的人或对话。不过我可以根据当前的输入来提供帮助和解答问题。如果您有具体的问题需要解答或者想要讨论某个话题，请告诉我，我会尽力为您提供所需的信息或解答。您现在想要了解什么内容呢？', id='f4c61394-9b5a-4809-beec-7ecf24aa62b0'), 'documents': None}}}, created_at='2024-07-24T09:51:42.466256+00:00', parent_config={'configurable': {'thread_id': '114', 'thread_ts': '1ef49a25-40b4-689a-8018-05ba51d8a530'}})"
      ]
     },
     "execution_count": 108,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "snapshot = app.get_state({\"configurable\": {\"thread_id\": \"114\"}})\n",
    "snapshot"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "ChatTTS",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
